/*
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "PythonInterface.h"
#include "OgreMath/OgreMatrix4.h"
#include "FractalNoiseGenerator.h"
#include "Spline.h"
#include "SplineToTree.h"
#include "MarchingCubes.h"
#include "Operators.h"
#include "ExportOBJ.h"
#include "ExportSTL.h"
#include "ExportPLY.h"
#include "Serializer.h"
#include "MeshImport/OBJReader.h"
#include "MeshImport/MeshToTree.h"

class ReleaseGIL
{
public:
    inline ReleaseGIL()
	{
        save_state = PyEval_SaveThread();
    }

    inline ~ReleaseGIL()
	{
        PyEval_RestoreThread(save_state);
    }
private:
    PyThreadState *save_state;
};

void PyVolume::exportAsObj(std::string fileName, float voxelsPerUnit, int numSmoothIterations)
{
	ReleaseGIL unlock;
	forceEval();
	MarchingCubes<MaterialID>::treeToMesh<ExportOBJ>(fileName, *m_Tree, voxelsPerUnit, numSmoothIterations, false);
}

void PyVolume::exportAsStl(std::string fileName, float voxelsPerUnit, int numSmoothIterations)
{
	ReleaseGIL unlock;
	forceEval();
	MarchingCubes<MaterialID>::treeToMesh<ExportSTL>(fileName, *m_Tree, voxelsPerUnit, numSmoothIterations);
}

void PyVolume::exportAsPly(std::string fileName, float voxelsPerUnit, int numSmoothIterations)
{
	ReleaseGIL unlock;
	forceEval();
	MarchingCubes<MaterialID>::treeToMesh<ExportPLY>(fileName, *m_Tree, voxelsPerUnit, numSmoothIterations);
}

/*void setMaterialForNode(SolidTree::Node& node, const MaterialID& material)
{
	node.setData(material);
	if (node.getType() == Both)
	{
		for (unsigned int i = 0; i < 8; i++)
			setMaterialForNode(node.getSubNode(i), material);
	}
}*/

void PyVolume::setMaterial(const MaterialID& material)
{
	m_MaterialID = material;
	m_NeedsUpdate = true;
	/*forceEval();
	for (unsigned int i = 0; i <8; i++)
		setMaterialForNode(m_Tree->getRoot(i), material);*/
}

PyVolume PyVolume::unionOp(PyVolume &other)
{
	ReleaseGIL unlock;
	forceEval();
	other.forceEval();
	PyVolume out;
	Operators::booleanOp<Operators::OpUnion<Vector3i, MaterialID>, Vector3i, MaterialID, MaterialID, MaterialID>(*out.m_Tree, *m_Tree, *other.m_Tree);
	return out;
}
PyVolume PyVolume::differenceOp(PyVolume &other)
{
	ReleaseGIL unlock;
	forceEval();
	other.forceEval();
	PyVolume out;
	Operators::booleanOp<Operators::OpDifference<Vector3i, MaterialID>, Vector3i, MaterialID, MaterialID, MaterialID>(*out.m_Tree, *m_Tree, *other.m_Tree);
	return out;
}
PyVolume PyVolume::intersectionOp(PyVolume &other)
{
	ReleaseGIL unlock;
	forceEval();
	other.forceEval();
	PyVolume out;
	Operators::booleanOp<Operators::OpIntersection<Vector3i, MaterialID>, Vector3i, MaterialID, MaterialID, MaterialID>(*out.m_Tree, *m_Tree, *other.m_Tree);
	return out;
}

PyVolume PyVolume::thicken(float radius)
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out;
	std::cout << "start thicken!" << std::endl;
	Operators::offset(*out.m_Tree, *m_Tree, radius);
	std::cout << "end thicken!" << std::endl;
	return out;
}

PyVolume PyVolume::roundConcaveOp(float radius)
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out;
	Operators::roundConcave(*out.m_Tree, *m_Tree, radius);
	return out;
}

PyVolume PyVolume::roundConvexOp(float radius)
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out;
	Operators::roundConvex(*out.m_Tree, *m_Tree, radius);
	return out;
}

PyVolume PyVolume::offsetOp(float radius)
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out;
	Operators::offset(*out.m_Tree, *m_Tree, radius);
	return out;
}

PyVolume PyVolume::insetOp(float radius)
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out;
	Operators::inset(*out.m_Tree, *m_Tree, radius);
	return out;
}

PyVolume PyVolume::surfels()
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out;
	Operators::surfels(*out.m_Tree, *m_Tree);
	return out;
}

void PyVolume::clear()
{
	m_Tree = std::make_shared<SolidTree>();
}

PyVolume PyVolume::copy()
{
	ReleaseGIL unlock;
	forceEval();
	PyVolume out(std::make_shared<SolidTree>(*m_Tree));
	return out;
}

void PyVolume::setPosition(const Ogre::Vector3 &_position)
{
	m_Position = _position;
	m_NeedsUpdate = true;
}
void PyVolume::setScale(const Ogre::Vector3 &_scale)
{
	m_Scale = _scale;
	m_NeedsUpdate = true;
	m_WasTransformed = true;
}
void PyVolume::setRotation(float xRotDeg, float yRotDeg, float zRotDeg)
{
	Ogre::Matrix3 mat3;
	mat3.FromEulerAnglesXYZ(Ogre::Radian(Ogre::Degree(xRotDeg)), Ogre::Radian(Ogre::Degree(yRotDeg)), Ogre::Radian(Ogre::Degree(zRotDeg)));
	m_Orientation.FromRotationMatrix(mat3);
	m_NeedsUpdate = true;
	m_WasTransformed = true;
}

boost::python::tuple PyVolume::getExtents()
{
	ReleaseGIL unlock;
	forceEval();
	Box<Vector3i> box;
	NTreeHelpers::getExtents(box, *m_Tree);
	return boost::python::make_tuple(box.min.toOgreVec(), box.max.toOgreVec());
}

void PyVolume::updateCopyConMover()
{
	CopyCon<Vector3i, MaterialID> copyCon(*m_Tree);
	if (m_WasTransformed) copyConMover.setTransform(m_Position, m_Scale, m_Orientation, copyCon);
	else copyConMover.setMove(m_Position, copyCon);
}

void PyVolume::forceEval()
{
	if (m_NeedsUpdate)
	{
		std::cout << "Performance Warning: Transforming non-primitive object" << std::endl;
		updateCopyConMover();
		m_Tree = std::make_shared<SolidTree>(copyConMover, m_MaterialID);
	}
	m_NeedsUpdate = false;
}

/*void PyVolume::performExtrude(PyVolume &out, PyVolume &toExtrude)
{
	std::cout << "Performance Warning: Extruding with non-primitive object" << std::endl;
	updateCopyConMover();
	Operators::extrude<Vector3i, MaterialID, MaterialID, SimpleGeneralExtruder, TransformConstructor<CopyCon<Vector3i, MaterialID>>>(*out.getTree(), *toExtrude.getTree(), SimpleGeneralExtruder(copyConMover));
}*/
void PyVolume::performExtrudeStep(SolidTree &outTree, const Vector3i &offset)
{
	std::cout << "Performance Warning: Extruding with non-primitive object" << std::endl;
	m_Position = m_Position + offset.toOgreVec();
	updateCopyConMover();
	outTree.raise(copyConMover.getSize());

	Area<Vector3i> subs[8];
	outTree.subAreas(subs);
	for(unsigned int i=0; i<8; i++)
		Operators::extrudeNodes(outTree.getRoot(i), subs[i], copyConMover, m_MaterialID);
	m_Position = m_Position - offset.toOgreVec();
}

int PyVolume::countNodes()
{
	ReleaseGIL unlock;
	forceEval();
	return NTreeHelpers::countNodes(*m_Tree);
}

int PyVolume::countVoxels()
{
	ReleaseGIL unlock;
	forceEval();
	Vector3i v;
	return NTreeHelpers::countVoxels(*m_Tree, v);
}

Ogre::Vector3 PyVolume::centerOfMass()
{
	ReleaseGIL unlock;
	forceEval();
	Vector3i v;
	NTreeHelpers::countVoxels(*m_Tree, v);
	return v.toOgreVec();
}

void PyVolume::checkTree()
{
	ReleaseGIL unlock;
	forceEval();
	Vector3i v;
	NTreeHelpers::checkTree(*m_Tree);
}

template<class T, class T2>
class PyPrimitiveObject : public PyVolume
{
protected:
	T baseConstructor;
	TransformConstructor<T> baseConMover;

public:
	PyPrimitiveObject(const T &_baseConstructor) : PyVolume(), baseConstructor(_baseConstructor) { m_NeedsUpdate = true; }
	virtual ~PyPrimitiveObject() {}

	TransformConstructor<T>* getConstructor()
	{
		if (m_WasTransformed) baseConMover.setTransform(m_Position, m_Scale, m_Orientation, baseConstructor);
		else baseConMover.setMove(m_Position, baseConstructor);
		return &baseConMover;
	}

	void forceEval()
	{
		if (m_NeedsUpdate) m_Tree = std::make_shared<SolidTree>(*getConstructor(), m_MaterialID);
		m_NeedsUpdate = false;
	}

	/*void performExtrude(PyVolume &out, PyVolume &toExtrude)
	{
		Operators::extrude<Vector3i, MaterialID, MaterialID, SimplePrimitiveExtruder, TransformConstructor<T>>(*out.getTree(), *toExtrude.getTree(), SimplePrimitiveExtruder(this));
	}*/

	void performExtrudeStep(SolidTree &outTree, const Vector3i &offset) override
	{
		m_Position = m_Position + offset.toOgreVec();
		TransformConstructor<T> *con = getConstructor();
		outTree.raise(con->getSize());

		Area<Vector3i> subs[8];
		outTree.subAreas(subs);
		for(int i=0; i<8; i++)
			Operators::extrudeNodes(outTree.getRoot(i), subs[i], *con, m_MaterialID);
		m_Position = m_Position - offset.toOgreVec();
	}

	/*class SimplePrimitiveExtruder
	{
	private:
		PyPrimitiveObject<T, T2> *object;

	public:
		SimplePrimitiveExtruder(PyPrimitiveObject<T, T2> *_object) : object(_object) {}
		std::pair<TransformConstructor<T>, MaterialID> fn(float fPos) const
		{
			return std::make_pair(*object->getConstructor(), object->m_MaterialID);
		}
	};*/
};

class PySphere : public PyPrimitiveObject<SphereCon<Vector3i>, PySphere>
{
public:
	PySphere(float _radius) : PyPrimitiveObject(SphereCon<Vector3i>(_radius, Vector3i(0))) {}
};

class PyBox : public PyPrimitiveObject<BoxCon<Vector3i>, PyBox>
{
public:
	PyBox(const Ogre::Vector3 &extents) : PyPrimitiveObject(BoxCon<Vector3i>(extents)) {}
};

class PySpline : public PyVolume
{
public:
	PySpline(const boost::python::list &splinePointsPython, bool closed, const boost::python::object& extruder) : PyVolume()
	{
		std::vector<Ogre::Vector3> splinePoints;
		for (int i = 0; i < boost::python::len(splinePointsPython); i++)
			splinePoints.push_back(boost::python::extract<Ogre::Vector3>(splinePointsPython[i]));
		Spline spline(splinePoints, closed);
		SplineToTree::extrudeSplineIntoTree(*m_Tree, spline, PythonExtruder(extruder));
	}
};

class PyFractalNoise : public PyPrimitiveObject<FractalNoiseGenerator::Constructor, PyFractalNoise>
{
public:
	PyFractalNoise(int expo, int heightRange, float roughness, bool flat) : PyPrimitiveObject(FractalNoiseGenerator::Constructor(expo, heightRange, roughness, flat)) {}
};

/*PyVolume PyVolume::extrude(PyVolume &other)
{
	forceEval();
	PyVolume out;
	other.performExtrude(out, *this);
	return out;
}*/

void PyVolume::PythonExtruder::extrude (SolidTree &out, const Vector3i position, float fPos) const
{
	boost::python::object object = extruder(fPos);
	boost::python::extract<PySphere*> isSphere(object);
	if (isSphere.check()) { isSphere()->performExtrudeStep(out, position); return; }

	boost::python::extract<PyBox*> isBox(object);
	if (isBox.check()) { isBox()->performExtrudeStep(out, position); return; }

	boost::python::extract<PyFractalNoise*> isNoise(object);
	if (isNoise.check()) { isNoise()->performExtrudeStep(out, position); return; }

	//fallback
	boost::python::extract<PyVolume*> isVolume(object);
	isVolume()->performExtrudeStep(out, position);
}

class PyArray : public PyVolume
{
public:
	PyArray(const Ogre::Vector3 &vShift, int iXReps, int iYReps, int iZReps)
	{
		int aiCounts[3]={iXReps, iYReps, iZReps};
		NTreeHelpers::createArray<Vector3i, MaterialID>(*m_Tree, Vector3i((int)vShift.x, (int)vShift.y, (int)vShift.z), m_MaterialID, aiCounts);
	}
};

class PyMesh : public PyVolume
{
public:
	PyMesh(const std::string &meshName, float voxelsPerUnit)
	{
		std::vector<Ogre::Vector3> vertices, vertexNormals;
		std::vector<unsigned int> indices;
		std::ifstream file(meshName);
		std::cout << "Reading mesh file " << meshName << "..." << std::endl;
		OBJReader::readObjFile(file, vertices, vertexNormals, indices);
		MeshToTree::insertMeshVolumeIntoTree<MaterialID>(*m_Tree, vertices, indices, voxelsPerUnit);
	}
};

/*PyVolume PyVolume::extrudeDynamic(const boost::python::object &extruder)
{
	forceEval();
	PyVolume out;
	Operators::extrude<Vector3i, MaterialID, MaterialID, PythonExtruder>(*out.m_Tree, *m_Tree, PythonExtruder(extruder));
	return out;
}*/

BOOST_PYTHON_MODULE(vBaum)
{
	class_<Ogre::Vector3>("Vector3", init<float, float, float>())
		.def(self + self)
		.def(-self)
		.def(self - self)
		.def(self * self)
		.def(self / self)
		.def(self * float())
		.def(self / float())
		.def("normalise", &Ogre::Vector3::normalise)
		.def("squaredLength", &Ogre::Vector3::squaredLength)
		.def("length", &Ogre::Vector3::length)
		.def("dotProduct", &Ogre::Vector3::dotProduct)
		.def("crossProduct", &Ogre::Vector3::crossProduct)
		.def("distance", &Ogre::Vector3::distance)
		.def("squaredDistance", &Ogre::Vector3::squaredDistance)
		.add_property("x", &Ogre::Vector3::getX, &Ogre::Vector3::setX)
		.add_property("y", &Ogre::Vector3::getY, &Ogre::Vector3::setY)
		.add_property("z", &Ogre::Vector3::getZ, &Ogre::Vector3::setZ);

	class_<Ogre::Quaternion>("Quaternion", init<float, Ogre::Vector3>())
		.def(self * self)
		.def(self * Ogre::Vector3());

	class_<PyVolume>("Volume", init<>())
		.def("clear", &PyVolume::clear)
		.def("copy", &PyVolume::copy)
		.def("setPosition", &PyVolume::setPosition)
		.def("setRotation", &PyVolume::setRotation)
		.def("setScale", &PyVolume::setScale)
		.def("exportAsObj", &PyVolume::exportAsObj)
		.def("exportAsStl", &PyVolume::exportAsStl)
		.def("exportAsPly", &PyVolume::exportAsPly)
		.def("union", &PyVolume::unionOp)
		.def("intersect", &PyVolume::intersectionOp)
		.def("subtract", &PyVolume::differenceOp)
		.def("roundConcave", &PyVolume::roundConcaveOp)
		.def("roundConvex", &PyVolume::roundConvexOp)
		.def("inset", &PyVolume::insetOp)
		.def("offset", &PyVolume::offsetOp)
		.def("setColor", &PyVolume::setMaterial)
		// .def("extrudeDynamic", &PyVolume::extrudeDynamic)
		// .def("extrude", &PyVolume::extrude)
		.def("thicken", &PyVolume::thicken)
		.def("surfels", &PyVolume::surfels)
		.def("countNodes", &PyVolume::countNodes)
		.def("countVoxels", &PyVolume::countVoxels)
		.def("getExtents", &PyVolume::getExtents)
		.def("centerOfMass", &PyVolume::centerOfMass)
		.def("checkTree", &PyVolume::checkTree);

	class_<PySphere, bases<PyVolume>>("Sphere", init<float>());

	class_<PyBox, bases<PyVolume>>("Box", init<Ogre::Vector3>());

	class_<PySpline, bases<PyVolume>>("Spline", init<boost::python::list, bool, const boost::python::object&>());

	class_<PyArray, bases<PyVolume>>("Array", init<Ogre::Vector3, int, int, int>());
	class_<PyFractalNoise, bases<PyVolume>>("FractalNoise", init<int, int, float, bool>());

	class_<PyMesh, bases<PyVolume>>("Mesh", init<const std::string&, float>());

}
